//Create global namespace Happy
if (typeof Happy == "undefined" || !Happy) {
  var Happy = {};
}

//Module for $=jQuery
(function($) {
//Create namespace Happy.WidgetExpand
//Make widgets expandable
//Title (click to toggle content): h2:first
//Content: div.widget-content
//Prepend span.widget-expand to title
Happy.WidgetExpand = (function(){
  //Public members
  return {
    //Widgets to be expandable. Need be Object with two properties.
    //css: selector string of the widget
    //close: boolean for widget content hide or show
    TARGET_WIDGET: [],
    //Symbol for expandable widget
    OPEN_SYMBOL: '[-]',
    CLOSE_SYMBOL: '[+]',
    
    //Make target widgets expandable
    attachIt: function() {
      var we = Happy.WidgetExpand;
      $(we.TARGET_WIDGET).each(function(i,target){
        //Create expandable symbol and hide/show the widget content
        var expand = $('<span class="widget-expand"></span>');
        if (target.close) {
          expand.html(we.CLOSE_SYMBOL).addClass('expand-close');
          $(target.css + " div.widget-content").hide();
        } else {
          expand.html(we.OPEN_SYMBOL).addClass('expand-open');
          $(target.css + " div.widget-content").show();
        }
        //Click widget title to toggle conent
        $(target.css + " h2").eq(0).prepend(expand).bind('click', function(){
          if ($(target.css + " div.widget-content").is(':hidden')) {
            $(target.css + " span.widget-expand").html(we.OPEN_SYMBOL).removeClass('expand-close').addClass('expand-open');
          } else {
            $(target.css + " span.widget-expand").html(we.CLOSE_SYMBOL).removeClass('expand-open').addClass('expand-close');
          }
          $(target.css + " div.widget-content").toggle();
        });
      });//end of each
    }//end of function
  };//end of return
})();

//Create namespace Happy.LabbelCloud
Happy.LabelCloud = (function(){
  //Private members
  
  //Regular expression for label count
  var regexCount = /\((\d+)\)/;
  
  //Public members
  return {
    //font size range
    MAX_SIZE: 25,
    MIN_SIZE: 13,
    //font color range
    MAX_COLOR: [255,74,74],
    MIN_COLOR: [40,200,20],
    //Element id to apply label cloud
    TARGET_ID: "#Label1",
    
    //Weighting the style value according count and maxCount
    weightStyle: function( min, max, count, maxCount ) {
      if (maxCount <= 1 || min === max ) {
        return max;
      } else if (min < max) {
        return min + Math.floor(Math.log(count) * (max-min) / Math.log(maxCount));
      } else {
        return min - Math.floor(Math.log(count) * (min-max) / Math.log(maxCount));
      }//end of else
    },
    
    weightStyleSectional: function( min, max, count, maxCount ) {
      if (maxCount <= 1 || min === max ) {
        return max;
      } else {
        //Scale down count smaller than (0.66*maxCount) to make more close-to-MIN_COLORed labels 
        if (count < maxCount*0.66) {
          count = (count-1)*0.3 + 1;
        }
        if (min < max) {
          return min + Math.floor(Math.log(count) * (max-min) / Math.log(maxCount));
        } else {
          return min - Math.floor(Math.log(count) * (min-max) / Math.log(maxCount));
        }
      }//end of else
    },
    
    cloudIt: function(weightFn) {
      //Check weighting funciton
      if (!$.isFunction(weightFn)) {
        return;
      }
      
      //Label link jQuery objects
      var labelObjs = [];
      //Label counts
      var labelCounts = [];
      //Maximum label count
      var maxCount = 0;      
      var aObj, sObj, count, s, r, g, b;    
      
      //find targets
      $(this.TARGET_ID + " li").each(function(){
        //Label link
        aObj = $(this).find("a");
        //Label count
        sObj = $(this).find("span");
        count = parseInt(regexCount.exec(sObj.text())[1]);
        labelObjs.push(aObj);
        labelCounts.push(count);
        if (count > maxCount) maxCount = count;
      });
      
      //add style
      $(this.TARGET_ID + " div.widget-content").addClass("label-cloud");
      for(var i=0; i<labelObjs.length; i++) {
        aObj = labelObjs[i];
        count = labelCounts[i];
        s = weightFn( this.MIN_SIZE, this.MAX_SIZE, count, maxCount );
        r = weightFn( this.MIN_COLOR[0], this.MAX_COLOR[0], count, maxCount );
        g = weightFn( this.MIN_COLOR[1], this.MAX_COLOR[1], count, maxCount );
        b = weightFn( this.MIN_COLOR[2], this.MAX_COLOR[2], count, maxCount );
              
        aObj.css('color', 'RGB('+r+','+g+','+b+')').attr('title', count + ' posts').parent("li").css('font-size', s + "px");;
      }//end of for
      
    }//end of function
  };//end of return
})();

//Module for Happy.Feed
//Make result showcase for json format feed
(function() {
  //Create namespace Happy.Feed and shortcut fd
  Happy.Feed = Happy.Feed || {};
  var fd = Happy.Feed;

  //Private members
  
  //Not showing all the feed entires at once, but dividing them into pages.
  //Start index of feed entry
  var startIndex = 0;
  //Total number of feed entires
  var totalResults = 0;
  //Number of feed entries per page
  var itemsPerPage = 0;
  //Total number of pages
  var totalPages = 0;
  //Regular expression for converting ISO Date format to Javascript Date formate
  var regexISODate = /(\d{4})-(\d{2})-(\d{2})T(\d{2}:\d{2}:\d{2})\.\d{3}(.\d{2}):(\d{2})/g;
  //Regular expression for getting site url and feed url
  var regexQueryUrl = /(http:\/\/[^\/]*)?\/feeds\/posts\/summary\/(?:-\/)*([^\?]*)\?/;
  //Regular expression for escape special characters in summary
  var regexLt = /</g;
  var regexGt = />/g;
  
  //Escape HTML tag special characters
  var _escapeHTML = function(str) {
    return str.replace(regexLt,"&lt;").replace(regexGt,"&gt;");
  };
  
  //ISO Date conversion to LocaleString
  var _isoDateLocaleString = function(dateString) {
    return (new Date(dateString.replace(regexISODate,"$1/$2/$3 $4"))).toLocaleString();
  };
  
  //Make navigation control that shows all page links
  //@param cursorFn {Function} Page link click function for querying another page. Require 2 params: start index, urlQueryOptions.
  //@param urlQueryOptions {String} Query url including query options. Used as param of cursorFn.
  var _makeNav = function(cursorFn, urlQueryOptions) {
    if (!$.isFunction(cursorFn)) {
      return null;
    }
    
    var feedCursorBox, feedCursorPage, index = 0;
  
    feedCursorBox = document.createElement("div");
    feedCursorBox.className = "feed-cursor-box";
    
    //event delegation
    $(feedCursorBox).bind("click", function(e) {
      var t = e.target;
      var s = 0;
      if ($(t).not("span.feed-cursor-current-page").is("span.feed-cursor-page")) {
        s = (parseInt($(t).text()) - 1) * itemsPerPage + 1;
        cursorFn(s, urlQueryOptions);
        e.stopPropagation();
      }
    });
  
    for(var k=0; k<totalPages; k++) {
      index = (k * itemsPerPage) + 1;

      feedCursorPage = document.createElement("span");
      feedCursorPage.innerHTML = k+1;
      //Style current page link
      if (index <= startIndex && startIndex < (index+itemsPerPage)) {
        feedCursorPage.className = "feed-cursor-page feed-cursor-current-page";
      } else {
        feedCursorPage.className = "feed-cursor-page";
      }
    
      feedCursorBox.appendChild(feedCursorPage);
      //Seperate by blank for automatic changing line
      feedCursorBox.appendChild(document.createTextNode(" "));
    }
  
    return feedCursorBox;
  };
  
  //Make navigation control that simply show previous and next page link, current start index, total results.
  //Can chang the start index for new query.
  //@param cursorFn {Function} Page link click function for querying another page. Require 2 params: starindex, urlQueryOptions.
  //@param urlQueryOptions {String} Query url including query options. Used as param of cursorFn.
  var _makeSimpleNav = function(cursorFn, urlQueryOptions) {
    if (!$.isFunction(cursorFn)) {
      return null;
    }
    
    var feedCursorBox, strBuilder = "";
  
    feedCursorBox = document.createElement("div");
    feedCursorBox.className = "feed-cursor-box";
    
    //event delegation
    $(feedCursorBox).bind("click", function(e) {
      var t = e.target;
      var s = 0;
      if ($(t).is("span.feed-cursor-prev")) {
        s = (startIndex > itemsPerPage) ? startIndex - itemsPerPage : 1;
        cursorFn(s, urlQueryOptions);
        e.stopPropagation();
      } else if ($(t).is("span.feed-cursor-next")) {
        s = startIndex + itemsPerPage;
        cursorFn(s, urlQueryOptions);
        e.stopPropagation();
      }
    });
    
    if (startIndex > 1) {
      strBuilder += '<span class="feed-cursor-prev">Prev</span>';
    }
    strBuilder += ' ( ';
    if (itemsPerPage >= totalResults) {
      strBuilder += startIndex;
    } else {
      strBuilder += '<input type="text" value="';
      strBuilder += startIndex;
      strBuilder += '" size="3" class="feed-cursor-input" />';
    }
    strBuilder += ' / ' + totalResults + ' ) ';
    if ((startIndex + itemsPerPage) <= totalResults) {
      strBuilder += '<span class="feed-cursor-next">Next</span>';
    }
    
    feedCursorBox.innerHTML = strBuilder;
    strBuilder = null;
    
    $("input.feed-cursor-input", feedCursorBox).bind("change", function(e) {
      var t = e.target;
      var s = $(t).val();
      //Check format and range of the input start index
      if (isNaN(s) || s < 1 || s > totalResults) {
        $(t).val(startIndex);
      } else {
        cursorFn(parseInt(s), urlQueryOptions);
      }
      e.stopPropagation();
    });
    
    //Press enter to trigger change event
    if ($.browser.msie || $.browser.opera) {
      $("input.feed-cursor-input", feedCursorBox).bind('keypress', function(e) {
        var t = e.target;
        //enter
        if (e.which == 13) {
          $(t).change();
        }
      });
    }
    
    return feedCursorBox;
  };
  
  //Make no navigation control
  var _makeNoneNav = function() { return null; };
  
  //Public members
  $.extend( Happy.Feed, {
    //Feed entry summery length
    SNIPPET_LENGTH: 120,
    //Feed entry related labels
    PUBLISHED_LABEL: "發布日期： ",
    UPDATED_LABEL: "更新日期： ",
    TAGS_LABEL: "標籤： ",
    //Feed link image. Defualt: feedburner feed image.
    FEED_IMG_URL: "http://www.feedburner.com/fb/images/pub/feed-icon16x16.png",
    
    //Get the link href with the first matching rel property
    //@param linkArray {Array} Array of link objects
    //@param relStr {String} Target rel property
    getHref: function(linkArray, relStr) {
      var j = 0;
      while(j < linkArray.length && linkArray[j].rel != relStr)
        j++;
      return linkArray[j].href;
    },

    //Make feed entries showcase
    //@param data {Object} Feed data
    //@param navOption {String} Navigation control option: "all"    default, show all page links.
    //                                                     "simple" show previous and next page links, current start index, total entires.
    //                                                     "none"   no navigation control.
    //@param cursorFn {Function} Page link click function for querying another page. Require 2 params: starindex, urlQueryOptions.
    //@param urlQueryOptions {String} Query url including query options. Used as param of cursorFn.
    makeEntries: function(data, navOption, cursorFn, urlQueryOptions) {
      //Check feed data existence, type, and total.
      if (typeof data == "undefined" || !data) {
        return null;
      } else if (typeof data.feed.entry == "undefined" || !data.feed.entry) {
        return null;
      } else if (data.feed.openSearch$totalResults.$t == 0) {
        return null;
      }
      
      startIndex = parseInt(data.feed.openSearch$startIndex.$t);
      itemsPerPage = parseInt(data.feed.openSearch$itemsPerPage.$t);
      totalResults = parseInt(data.feed.openSearch$totalResults.$t);
      totalPages = Math.ceil(totalResults/itemsPerPage);
      
      var entries = data.feed.entry;
      var entry;
      //Use array join for efficiency in IE
      var strBuilder = [];
      
      var execArray = regexQueryUrl.exec(urlQueryOptions);
      //Site url
      var baseUrl = execArray[1] || fd.getHref(data.feed.link, 'alternate');
      //Feed url
      var feedUrl = execArray[0].slice(0,-1);
      //Labels
      var category = execArray[2] ? decodeURI(execArray[2]).split("/") : undefined;
      
      var feedEntries = document.createElement("div");
      feedEntries.className = "feed-entries";

      strBuilder.push('<div class="feed-blog-box"><div class="feed-blog-title"><a href="');
      strBuilder.push(baseUrl);
      strBuilder.push('" target="_blank">');
      strBuilder.push(data.feed.title.$t);
      strBuilder.push('</a><a href="');
      strBuilder.push(feedUrl);
      strBuilder.push('" target="_blank" type="application/rss+xml"><img src="');
      strBuilder.push(fd.FEED_IMG_URL);
      strBuilder.push('" alt="(Feed)"/></a></div>');    
      if (category) {
        strBuilder.push('<div class="feed-tags-box">');
        if (category.length > 0) {
          strBuilder.push('<span class="feed-tag">');
          strBuilder.push(category[0]);
          strBuilder.push('</span>');
        }
        for(var j=1; j<category.length; j++) {
          strBuilder.push(', <span class="feed-tag">');
          strBuilder.push(category[j]);
          strBuilder.push('</span>');
        }
        strBuilder.push('</div>');
      }//end of if category
      strBuilder.push('</div>');
      
      for(var i=0; i<entries.length; i++) {
        entry = entries[i];        
        strBuilder.push('<div class="feed-entry">');
        strBuilder.push('<div class="feed-title"><a href="');
        strBuilder.push(fd.getHref(entry.link, 'alternate'));
        strBuilder.push('" target="_blank">');
        strBuilder.push(entry.title.$t);
        strBuilder.push('</a></div>');
        strBuilder.push('<div class="feed-published">');
        strBuilder.push(fd.PUBLISHED_LABEL);
        strBuilder.push(_isoDateLocaleString(entry.published.$t));
        strBuilder.push('</div>');
        strBuilder.push('<div class="feed-updated">');
        strBuilder.push(fd.UPDATED_LABEL);
        strBuilder.push(_isoDateLocaleString(entry.updated.$t));
        strBuilder.push('</div>');
        strBuilder.push('<div class="feed-snippet">');
        //Need to change: check the complete escape list
        strBuilder.push(_escapeHTML(entry.summary.$t.substr(0,fd.SNIPPET_LENGTH)));
        strBuilder.push(' ...</div>');
        strBuilder.push('<div class="feed-tags-box"><span class="feed-tags-label">');
        strBuilder.push(fd.TAGS_LABEL);
        strBuilder.push('</span>');
        //Check if no label
        if (entry.category) {
          if (entry.category.length > 0) {
            strBuilder.push('<span class="feed-tag">');
            strBuilder.push(entry.category[0].term);
            strBuilder.push('</span>');
          }
          for(var j=1; j<entry.category.length; j++) {
            strBuilder.push(', <span class="feed-tag">');
            strBuilder.push(entry.category[j].term);
            strBuilder.push('</span>');
          }
        }//end of if entry.category
        strBuilder.push('</div>');
        strBuilder.push('</div>');
      }//end of for
    
      feedEntries.innerHTML = strBuilder.join("");    
      strBuilder = null;
      
      //Append navigation control
      var makeNav = null;
      navOption = navOption || "all";
      if (navOption == "all") {
        makeNav = _makeNav;
      } else if (navOption == "simple") {
        makeNav = _makeSimpleNav;
      } else if (navOption == "none") {
        makeNav = _makeNoneNav;
      } else {
        makeNav = _makeNav;
      }
      feedEntries.appendChild(makeNav(cursorFn, urlQueryOptions));
      
      return feedEntries;
    }//end of makeEntries
  });//end of extend
})();//end of Happy.Feed module

//Module for Happy.MultiTagSearch
//Use Blogger Data API for multiple labels search
(function() {
  //Create namespace Happy.MultiTagSearch and shortcut mts
  Happy.MultiTagSearch = Happy.MultiTagSearch || {};
  var mts = Happy.MultiTagSearch;

  //Private members
  
  //Current query url, for check if firing multiple queries
  var urlCurrent = "";
  //Query related data
  var queries = {};
  //Query index
  var qIdx = 0;
  
  //Make query url
  _makeQueryUrl = function() {
    var tempUrl = "";
    var baseUrl = $.trim(mts.BASE_URL);
    //Encode labels
    var encodeCategory = encodeURI($.trim(mts.CATEGORY));
    
    //Check site url format
    if (baseUrl.length != 0) {
      if (baseUrl.indexOf("http://") == -1) {
        baseUrl = "http://" + baseUrl;
      }
      if (baseUrl.lastIndexOf("/") == baseUrl.length-1) {
        baseUrl = baseUrl.substr(0,baseUrl.length-1);
      }
      mts.BASE_URL = baseUrl;
    }
    
    //alt=json-in-script for jsonp
    tempUrl = baseUrl + "/feeds/posts/summary/" + (encodeCategory.length > 0 ? "-/" : "") + encodeCategory + "?alt=json-in-script&max-results=" + mts.MAX_RESULTS;
    
    //Data range and ordering. Default is ordered by published date.
    var strMin = "&published-min=";
    var strMax = "&published-max=";
    
    if (mts.ORDER_BY == "published") {
    } else if (mts.ORDER_BY == "updated") {
      tempUrl += "&orderby=updated";
      strMin = "&updated-min=";
      strMax = "&updated-max=";
    }
    
    if (mts.MIN_DATE.length > 0) {
      tempUrl += strMin + mts.MIN_DATE;
    }
    if (mts.MAX_DATE.length > 0) {
      tempUrl += strMax + mts.MAX_DATE;
    }
    
    return tempUrl;
  };
  
  //Garbage Clearing after done
  _clear = function(id) {
    //remove script tag
    $("#"+id).remove();
    var q = queries[id];
    if (q) {
      //remove callback
      mts[q.callback] = undefined;
      try { delete mts[q.callback]; } catch(e) {};
      //remove query data
      q = undefined;
      try { delete q; } catch(e) {};
    }
  };
  
  //Set up search parameters
  _getSearchFormValue = function() {
    mts.BASE_URL = $("#baseUrlBox").val();
    mts.CATEGORY = $('#tagsBox').val();
    mts.ORDER_BY = $("#mts-searchBox [name=orderBy]:checked").val();
    mts.MIN_DATE = $("#minDate").val();
    mts.MAX_DATE = $("#maxDate").val();
  };
  
  //Public members
  $.extend( Happy.MultiTagSearch, {
    //Site url
    BASE_URL: "",
    //Lables combination
    CATEGORY: "",
    MAX_RESULTS: 10,
    ORDER_BY: "published",
    MIN_DATE: "",
    MAX_DATE: "",
    SNIPPET_LENGTH: 120,
    //Query timeout for wrong site url and browsers not supporting onload event when script tag dosen't return javascript
    SUCCESS_TIMEOUT: 5000,
    //Extra timeout added for feed entries
    WEIGHTED_TIMEOUT: 50,
    //ID for loading message showcas
    TARGET_LOADING_MSG: "#mts-loadingMsg",
    //ID for query response showcase
    TARGET_QUERY_MSG: "#mts-msg",
    //Feed entries showcase related
    //target: ID for query results showcase
    //nav: navigation control type: "all", "simple", "none"
    //container: container of target, hide before updating query results
    TARGET_RESULTS: [{target:"#mts-searchResults", nav:"all", container:"#mts-searchControl"}],
    //Google search control
    GOOGLE_SEARCH_CONTROL: null,
    //Google web searcher
    GOOGLE_WEB_SEARCHER: null,
    
    //Make multiple labels search query using dynamic script tag and jsonp
    //@param startIndex {Integer} Start index of returned feed entry.
    //@param urlQueryOptions {String} Optional. Query url including query options. If not passed, make a new one by _makeQueryUrl(); 
    getCategory: function(startIndex, urlQueryOptions) {
      urlQueryOptions = urlQueryOptions || _makeQueryUrl();
      
      //Query data and script tag id
      var id = "mts" + qIdx;
      //JSONP callback name
      var callback = "jsonp" + qIdx;
      qIdx += 1;

      var url = urlQueryOptions + "&start-index=" + startIndex + "&callback=Happy.MultiTagSearch." + callback;
      
      urlCurrent = url;
      queries[id] = { url:url, callback:callback, timeoutID:null, done:false };
      
      //Message showcase
      $(mts.TARGET_LOADING_MSG).show();
      $(mts.TARGET_QUERY_MSG).hide();
      
      //JSONP callback. Use closure for remembering related data.
      //If called, it means query success.
      mts[callback] = (function(id, urlQueryOptions, callback) {
        return function(data) {
          clearTimeout(queries[id].timeoutID);
          queries[id].done = true;
          $(mts.TARGET_LOADING_MSG).hide();
          mts.showResult(data, urlQueryOptions);          
          _clear(id);
        };
      })(id, urlQueryOptions, callback);
      
      //Dynamic script tag
      var head = document.getElementsByTagName("head")[0];
      var script = document.createElement("script");
      script.src = url;
      script.id = id;
      
      //Query timeout callback for wrong site url and browsers not supporting onload event when script tag dosen't return javascript
      //If called, it means query taking too long, wrong site url, or such query not supported.
      queries[id].timeoutID = setTimeout((function(id, callback) {
        return function() {
          $(mts.TARGET_LOADING_MSG).hide();
          //Check for multiple queries. If not the current query, don't show the timeout message.
          if (queries[id].url === urlCurrent) {
            $(mts.TARGET_QUERY_MSG).text("取得資料時限已到").show();
          }  
          _clear(id);
        }; 
      })(id, callback), mts.SUCCESS_TIMEOUT + (mts.MAX_RESULTS * mts.WEIGHTED_TIMEOUT));
      
      //Onload event handler for browsers supporting onload event when script tag dosen't return javascript
      //If called, it means query complete (success or such query not supported)
      if (!$.browser.mozilla) {
        script.onload = script.onreadystatechange = (function(id) {
          return function() {
            var rs = this.readyState;
            if (!rs || "loaded" === rs || "complete" === rs) {
              clearTimeout(queries[id].timeoutID);
              $(mts.TARGET_LOADING_MSG).hide();
              //Check if query success
              if (!queries[id].done) {
                $(mts.TARGET_QUERY_MSG).text("無法取得資料").show();
              }
              _clear(id);
            }//end of if rs
          };
        })(id);
      }//end of if $.browser.mozilla
      
      head.appendChild(script);
      head = null;
      script = null;
    },
    
    //Show query results
    //@param data {Object} Feed data
    //@param urlQueryOptions {String} Query url including query options.
    showResult: function(data, urlQueryOptions) {
      var mtsMsg = "";
      
      //Check data existence, format, total entries.
      if (typeof data == "undefined" || !data) {
        mtsMsg = "無法取得資料";
      } else if (typeof data.feed == "undefined" || !data.feed) {
        mtsMsg = "資料格式錯誤";
      } else if (typeof data.feed.entry == "undefined" || !data.feed.entry || data.feed.openSearch$totalResults.$t == 0) {
        mtsMsg = "無符合資料";
      } else {
        Happy.Feed.SNIPPET_LENGTH = mts.SNIPPET_LENGTH;
        
        var mtsTarget;
        for(var i=0; i<mts.TARGET_RESULTS.length; i++) {
          mtsTarget = mts.TARGET_RESULTS[i];
          if ($(mtsTarget.target).length > 0) {
            $(mtsTarget.container).hide();
            $(mtsTarget.target).empty().append(Happy.Feed.makeEntries(data, mtsTarget.nav, mts.getCategory, urlQueryOptions));
            $(mtsTarget.container).show();
          }//end of if
        }//end of for

      }//end of if
      
      if (mtsMsg !== "") {
        $(mts.TARGET_QUERY_MSG).text(mtsMsg).show();
      }
    },
    
    //Make search form
    makeSearchForm: function() {    
      var mtsSearchForm = document.createElement("form");
      mtsSearchForm.id = "mts-searchBox";
      mtsSearchForm.className = "mts-search-box";
    
      var strBuilder = "";
      strBuilder += '<div class="mts-search-box">';
      strBuilder += '<div>';
      strBuilder += '<input type="text" id="tagsBox" class="mts-input ui-droppable" value="" title="格式：Label/Label/Label (ps.可從標籤列表拖放輸入,拖放時以滑鼠為準)">';
      strBuilder += '<input class="mts-search-button" type="submit" value="搜尋">';
      strBuilder += '</div><div class="mts-more-options">';
      strBuilder += '<span id="mts-msg" class="mts-msg" style="display:none"></span>';
      strBuilder += '<span id="mts-moreOptions">[+]選項</span>';
      strBuilder += '</div>';
      strBuilder += '</div>';
      
      strBuilder += '<div id="mts-searchOptions" class="mts-search-box" style="display: none;">';
      strBuilder += '<div>';
      strBuilder += '<label for="baseUrlBox">網址：</label><input type="text" id="baseUrlBox" class="mts-input" value="" title="只接受 Blogger.com 的 blog 網址">';
      strBuilder += '</div>';
      strBuilder += '<div>排序方式：';
      strBuilder += '<input type="radio" id="orderByPublished" value="published" checked="true" name="orderBy"><label for="orderByPublished">發布</label>';
      strBuilder += '<input type="radio" id="orderByUpdated" value="updated" name="orderBy"><label for="orderByUpdated">更新</label>';
      strBuilder += '</div>';
      strBuilder += '<div>';
      strBuilder += '<input type="text" id="minDate" class="mts-input dateRange" value="" size="12" title="格式：yyyy-mm-dd">';
      strBuilder += '≦日期＜';
      strBuilder += '<input type="text" id="maxDate" class="mts-input dateRange" value="" size="12" title="格式：yyyy-mm-dd">';
      strBuilder += '</div>';
      strBuilder += '</div>';
      
      mtsSearchForm.innerHTML = strBuilder;
      strBuilder = undefined;
    
      $("#mts-moreOptions", mtsSearchForm).bind("click", function(){
        $('#mts-searchOptions').slideToggle("fast", function(){
          if ($(this).is(":hidden")) {
            $("#mts-moreOptions").html("[+]選項");
          } else {
            $("#mts-moreOptions").html("[-]選項");
          }
        }); 
      });
      $(mtsSearchForm).bind("submit", function() {
        _getSearchFormValue();     
        mts.getCategory(1);    
        return false;
      });
  
      return mtsSearchForm;
    },
    
    //Add Droppable, Date picker effect for search form.
    //Add Draggable effect for labels.
    searchFormUIEffect: function() {
      //Use jquery ui data picker
      //Customize two date pickers to work as a date range 
      function customRange(input) {
        return {minDate: (input.id == 'maxDate' ? $('#minDate').datepicker('getDate') : null), 
                maxDate: (input.id == 'minDate' ? $('#maxDate').datepicker('getDate') : null)};
      }
      $("input.dateRange").datepicker({dateFormat: 'yy-mm-dd', beforeShow: customRange});
  
      //Making labels draggable and search input droppable
      $("#Label1 li").draggable({helper:'clone', cursorAt:{down:5, left:5}});
      $("#tagsBox").droppable({
        accept: "#Label1 li",
        tolerance: "pointer",
        hoverClass: "mts-draggable-hover",
        drop: function(ev, ui) {
          //If with Shift key, clear search input
          var tags = "";
          if (!ev.shiftKey) {
            tags = $.trim($(this).val());
            if (tags.length > 0) {
              tags += "/";
            }
          }
          //Remove label count
          $(this).val(tags + $.trim(ui.draggable.text().replace(/\(\d+\)/g,'')));
        }//end of drop
      });//end of droppable
    },
    
    //Make loading message
    makeLoadingMsg: function() {
      var mtsLoadingMsg = document.createElement("div");
      mtsLoadingMsg.id = "mts-loadingMsg";
      mtsLoadingMsg.className = "mts-loadingMsg";
      mtsLoadingMsg.style.display = "none";
      mtsLoadingMsg.innerHTML = "載入中...";
      return mtsLoadingMsg;
    },
    
    //Make search result holding place
    makeSearchControl: function() {
      var mtsSearchControl = document.createElement("div");
      mtsSearchControl.id = "mts-searchControl";
      mtsSearchControl.className = "mts-searchControl";
      mtsSearchControl.style.display = "none";
      mtsSearchControl.innerHTML = '<div id="mts-searchResults"></div><div id="mts-clearResults" class="mts-clear-button">&nbsp;</div>';
      $("#mts-clearResults", mtsSearchControl).bind("click", function(){ $("#mts-searchControl div.feed-entry").toggle(); });
      return mtsSearchControl;
    },
    
    //Make google custom search form holding place
    makeGoogleSearchForm: function() {
      var cseSearchForm = document.createElement("div");
      cseSearchForm.id = "cse-searchForm";
      return cseSearchForm;
    },
    
    //Make google custom search result holding place
    makeGoogleSearchControl: function() {
      var cseSearchControl = document.createElement("div");
      cseSearchControl.id = "cse-searchControl";
      cseSearchControl.className = "cse-searchControl";
      return cseSearchControl;
    },
    
    //Initialization
    //Set up mts search form, mts search result control, loading message block
    //Set up cse search from, cse search result control
    //Load google AJAX search API
    init: function() {
      //Make search form only if the default container #mts-searchForm exists
      if ($("#mts-searchForm").length == 0) {
        return;
      }
      
      //Make search form
      $("#mts-searchForm").empty().append(mts.makeSearchForm());
      //Add drag and drop effect and date picker
      mts.searchFormUIEffect();
      
      //Check to make mts query result showcase
      if ($("#mts-searchControl").length == 0) {
        $("#main").prepend(mts.makeSearchControl());
      } else {
        $("#mts-searchControl").replaceWith(mts.makeSearchControl());
      }
      
      //Check to make loading message showcase
      if ($("#mts-loadingMsg").length == 0) {
        $("#mts-searchControl").before(mts.makeLoadingMsg());
      }
      
      //Make linked cse search form only if the default container #cse-searchForm exists
      if ($("#cse-searchForm").length == 0) {
       return;
      }
      
      //Check to make cse query result showcase
      if ($("#cse-searchControl").length == 0) {
        $("#mts-searchControl").after(mts.makeGoogleSearchControl());
      }
      //Load google.search module. The callback will make cse search form and control.
      google.load("search", "1", {"callback":mts.onLoadGoogleSearch});
    },
    
    //Make linked custom search engine url
    makeCrefUrl: function() {
      var tempUrl = "";
      var baseUrl = $.trim(mts.BASE_URL);
      var category = $.trim(mts.CATEGORY);
      
      //Check site url format
      baseUrl = baseUrl.replace('http://', '');
      if (baseUrl.length > 0 && baseUrl.lastIndexOf("/") == baseUrl.length-1) {
        baseUrl = baseUrl.substr(0,baseUrl.length-1);
      }
      if (baseUrl.length == 0) {
        baseUrl = window.location.hostname;
      }
      
      //Set large max-results
      tempUrl = baseUrl + "/feeds/posts/summary/" + (category.length > 0 ? "-/" : "") + category + "?max-results=10000";
      
      //Data range and ordering. Default is ordered by published date.
      var strMin = "&published-min=";
      var strMax = "&published-max=";
      
      if (mts.ORDER_BY == "published") {
      } else if (mts.ORDER_BY == "updated") {
        tempUrl += "&orderby=updated";
        strMin = "&updated-min=";
        strMax = "&updated-max=";
      }
      
      if (mts.MIN_DATE.length > 0) {
        tempUrl += strMin + mts.MIN_DATE;
      }
      if (mts.MAX_DATE.length > 0) {
        tempUrl += strMax + mts.MAX_DATE;
      }
      
      return tempUrl;
    },
    
    //Before search callback
    //It's called for each searcher and we have only web searcher.
    onGoogleSearchStart: function(searchControl, searcher, query) {
      _getSearchFormValue();
      //Use url-based tool to make linked CSE definition file
      var crefUrl = "http://www.google.com/cse/tools/makecse?pattern=exact&url=" + encodeURIComponent(mts.makeCrefUrl());
      
      searcher.setSiteRestriction({"crefUrl":crefUrl});
    },
    
    //Google AJAX loader callback
    onLoadGoogleSearch: function() {
      //Create search control
      var sc = mts.GOOGLE_SEARCH_CONTROL = new google.search.SearchControl();
    
      //Add web searcher
      var ws = mts.GOOGLE_WEB_SEARCHER = new google.search.WebSearch();
      ws.setUserDefinedLabel("Multi-Labeled Keyword Search");
      ws.setResultSetSize(google.search.Search.LARGE_RESULTSET);
      ws.setLinkTarget(google.search.Search.LINK_TARGET_BLANK);
      var wsOptions = new google.search.SearcherOptions();
      wsOptions.setExpandMode(google.search.SearchControl.EXPAND_MODE_OPEN);
      sc.addSearcher(ws, wsOptions);
    
      //Position the search form root
      var scOptions = new google.search.DrawOptions();
      scOptions.setSearchFormRoot(document.getElementById("cse-searchForm"));
      //To set site restriction cref before search start
      sc.setSearchStartingCallback(mts, mts.onGoogleSearchStart);
      sc.draw(document.getElementById("cse-searchControl"), scOptions);
    }
  });//end of extend
})();//end of Happy.MultiTagSearch Module

//Module for Happy.RelatedPost
(function() {
  //Create namespace Happy.RelatedPost and shortcut rp
  Happy.RelatedPost = Happy.RelatedPost || {};
  var rp = Happy.RelatedPost;
  var fd = Happy.Feed;
  
  //Private members
  
  //Related post url hash for checking if it's already included.
  var hash = {};
  //Related post data
  var relatedPosts = [];
  //Related post count
  var postCount = 0;
  //Query index for script tag id
  var qIdx = 0;
  //Script tag ids
  var scriptIds = [];
  //Label index combinations
  var labelCombins = [];
  //Function references for querying next label combination and done with all combinations.
  var next;
  var done;
  //Regular expression for site url
  var regex = /(http:\/\/[^\/]*)\//;
  
  //Generate combinations. Not check if optimized.
  //@param a {Array} Collection of items to be selected.
  //@param l {Integer} Level of combinations. 
  //ex. a=[1,2,3], n=a.length
  //0: 1-combinations. [[1],[2],[3]]
  //1: n-combinations + 1-combinations. [[1,2,3],[1],[2],[3]]
  //2: n-combinations + (n-1)-combinations + 1-combinations. [[1,2,3],[1,2],[1,3],[2,3],[1],[2],[3]]
  //k: n-combinations ~ (n-k+1)-combinations + 1-combinations.
  var _combination = function(a, l) {
    var fn = function(n, src, got, all) {
      if (n == 0) {
        if (got.length > 0) {
          all[all.length] = got;
        }
        return;
      }
      for (var j = 0; j < src.length; j++) {
        fn(n - 1, src.slice(j + 1), got.concat([src[j]]), all);
      }
      return;
    }
    var all = [];
    if (l != 0) {
      all.push(a);
    }
    for (var i=a.length-1; i > a.length-l; i--) {
      fn(i, a, [], all);
    }
    for (var i=0; i < a.length; i++) {
      all.push([a[i]]);
    }
    return all;
  };
  
  //Make query url
  //@param labelCombin {Array} Label index combination
  var _makeQueryUrl = function(labelCombin) {
    var tempUrl = "";
    var baseUrl = $.trim(rp.BASE_URL);
    var category = "";
    
    //Check site url
    if (baseUrl.length != 0) {
      if (baseUrl.indexOf("http://") == -1) {
        baseUrl = "http://" + baseUrl;
      }
      if (baseUrl.lastIndexOf("/") == baseUrl.length-1) {
        baseUrl = baseUrl.substr(0,baseUrl.length-1);
      }
      rp.BASE_URL = baseUrl;
    }
    
    if (labelCombin.length > 0) {
      category += rp.LABELS[labelCombin[0]];
    }
    for(var i=1; i<labelCombin.length; i++) {
      category += "/" + rp.LABELS[labelCombin[i]];
    }
    
    //Max results + 1, because the target post is always included.
    tempUrl = baseUrl + "/feeds/posts/summary/-/" + category + "?max-results=" + (rp.MAX_POSTS+1);
    
    //Check using HTTPRequest or jsonp
    if (rp.TYPE === "local") {
      tempUrl += "&alt=json";
    } else {
      tempUrl += "&alt=json-in-script&callback=Happy.RelatedPost.saveRelated";
    }
    
    return tempUrl;
  };
  
  //Query next label combination with jsonp
  var _nextRemote = function() {
    //Check if more label combinations to query
    if (labelCombins.length == 0) {
      _doneRemote();
      return;
    }
    var queryUrl = _makeQueryUrl(labelCombins.shift());
    var head = document.getElementsByTagName("head")[0];
    var script = document.createElement("script");
    script.src = queryUrl;
    script.id = "related" + (qIdx++);
    scriptIds.push(script.id);
    head.appendChild(script);
  };
  
  //Done with jsonp
  var _doneRemote = function() {
    //Remove script tags
    for(var i=0; i<scriptIds.length; i++) {
      $("#"+scriptIds[i]).remove();
    }
    scriptIds = [];
    $(rp.TARGET).append(rp.makeRelated());
  };
  
  //Query next label combination with HTTPRequest
  var _nextLocal = function() {
    //Check if more label combinations to query
    if (labelCombins.length == 0) {
      _doneLocal();
      return;
    }
    
    var queryUrl = _makeQueryUrl(labelCombins.shift());
    $.ajax({
      url: queryUrl,
      dataType: "json",
      success: rp.saveRelated,
      //Still need to check
      error: function(){ _nextLocal(); }
    });
  };
  
  //Done with HTTPRequest
  var _doneLocal = function() {
    $(rp.TARGET).append(rp.makeRelated());
  };
  
  //Public members
  $.extend( Happy.RelatedPost, {
    //Site utl, automatic generated.
    BASE_URL: "",
    //Target post url
    POST_URL: "",
    //Max number of related posts
    MAX_POSTS: 5,
    //Leve of label combinations to query
    //ex. LABELS = ['AAA','BBB','CCC'];
    //0: 'AAA','BBB','CCC'
    //1: 'AAA/BBB/CCC','AAA','BBB','CCC'
    //2: 'AAA/BBB/CCC','AAA/BBB',AAA/CCC','BBB/CCC','AAA','BBB','CCC'
    LEVEL: 1,
    //Labels
    LABELS: [],
    TARGET: "",
    //"remote": query with dynamic script tag and jsonp
    //"local": query with HTTPRequest
    TYPE: "remote",
    RELATED_LABEL: "可能相關文章：",
    
    //Callback for Query
    //@param data {Object} Feed data.
    saveRelated: function(data) {
      if (typeof data.feed.entry == "undefined" || !data.feed.entry || data.feed.openSearch$totalResults.$t == 0) {
      } else {
        var entries = data.feed.entry;
        var href;
        
        for(var i=0; i<entries.length; i++) {
          href = fd.getHref(entries[i].link, 'alternate');
          if (typeof hash[href] == 'undefined') {
            hash[href] = postCount++;
            relatedPosts.push(entries[i]);
          }//end of if
        }//end of for
      }//end of if typeof
   
      if (postCount >= rp.MAX_POSTS) {
        //Done whith enough posts
        done();
      } else {
        //Query next combination
        next();
      }//end of if
    },
    
    getRelated: function() {
      //Get site url
      rp.BASE_URL = (regex.exec(rp.POST_URL))[1];
      
      var totalLabels = rp.LABELS.length;
      var indexArray = [];
      //Encode Lebles
      for(var i=0; i<totalLabels; i++) {
        rp.LABELS[i] = encodeURI(rp.LABELS[i]);
        indexArray.push(i);
      }  
      labelCombins = _combination(indexArray, rp.LEVEL);
      postCount = 0;
      hash = {};
      //Put target post url in hash to exclude it
      hash[rp.POST_URL] = -1;
      relatedPosts = [];
      
      //Check using HTTPRequest or jsonp
      if (rp.TYPE == "local") {
        next = _nextLocal;
        done = _doneLocal;
      } else {
        next = _nextRemote;
        done = _doneRemote;
      }
      
      next();
    },
    
    //Make related posts showcase
    makeRelated: function() {
      var entry;
      var strBuilder = '';
      //There maybe are more than MAX_POSTS posts in relatedPosts
      for(var i=0; i<rp.MAX_POSTS && i<postCount; i++) {
        entry = relatedPosts[i];
        //Only show yyyy-mm-dd
        strBuilder += '<li><span class="feed-published">' + entry.published.$t.substr(0,10) + '</span>';
        strBuilder += '<span class="feed-title"><a href="' + fd.getHref(entry.link, 'alternate') + '" target="_blank">' + entry.title.$t + '</a></span></li>';
      }//end of for
      var div = document.createElement("div");
      div.className = "related-post";
      if (postCount > 0) {
        strBuilder = '<div>' + rp.RELATED_LABEL + '</div><ul>' + strBuilder + '</ul>';
      }
      div.innerHTML = strBuilder;
      
      return div;
    }  
  });//end of extend
})();//end of Happy.RelatedPost module

})(jQuery);